package rs.etf.es.algorithm;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;

import rs.etf.es.algorithm.states.SendLambdaMessageState;
import rs.etf.es.algorithm.states.SendPiMessageState;

public class Node implements Serializable {

    public static final String NO_PARENT = "no parent";
    public static final String NO_CONDITION = "no condition";
    public static final Double NORMALIZATION_CONSTANT = new Double(1);

    private String name;
    private ConditionalProbabilityTable myCPT;
    private ArrayList<Node> parents;
    private ArrayList<Node> children;
    private ArrayList<String> values;
    private LambdaMessages lambdaMsgs;
    private PiMessages piMsgs;
    private HashMap<String, Double> lambdaValues;
    private HashMap<String, Double> piValues;
    private HashMap<String, Double> probabilities;

    public int x, y;
    public ArrayList<Node> tempParents = new ArrayList<Node>();

    public static Node selectedNode;

    public Node(String name) {
	this.name = name;
	myCPT = new ConditionalProbabilityTable();
	parents = new ArrayList<Node>();
	children = new ArrayList<Node>();
	values = new ArrayList<String>();
	lambdaMsgs = new LambdaMessages();
	piMsgs = new PiMessages();
	lambdaValues = new HashMap<String, Double>();
	probabilities = new HashMap<String, Double>();
	piValues = new HashMap<String, Double>();
    }

    public Node() {
	this("");
    }

    public void setAllValues(String[] vals) {
	for (String s : vals) {
	    values.add(s);
	}
    }

    /**
     * This method is invoked on node that receives message
     * 
     * @param sender
     *            is node that sends pi message
     */
    public void sendPiMessageToThisNode(Node sender, Algorithm algorithm) {

	JTextAreaLogger.instance().log("Sending pi message from variable " + sender.getName() + " to variable " + getName());

	for (String value : sender.getValues()) {
	    Double newPiMessageValue = sender.getPiValues().get(value);
	    Double product = new Double(1);

	    for (Node child : sender.getChildren()) {
		if (child != this) {
		    Double lambdaMessageValue = sender.getLambdaMsgs().getLambdaMessage(child.getName(), value);
		    product *= lambdaMessageValue;
		}
	    }

	    product *= newPiMessageValue;
	    JTextAreaLogger.instance().log("Calculating pi message: from " + sender.getName() + " to " + getName() + " - " + value + " = " + Util.getDoubleAsString(product));
	    this.piMsgs.putPiMessage(sender.getName(), value, product);
	}

	// calculate pi values
	for (String value : this.getValues()) {
	    Double sum = new Double(0);

	    for (String senderVal : sender.getValues()) {
		Double piValue = myCPT.getProbability(sender.getName(), value, senderVal);
		piValue *= piMsgs.getPiMessage(sender.getName(), senderVal);
		sum += piValue;
	    }

	    JTextAreaLogger.instance().log("Calculating pi value for " + value + " in variable " + getName() + " = " + Util.getDoubleAsString(sum));
	    piValues.put(value, sum);

	    Double conditionalProbability = getLambdaValues().get(value) * getPiValues().get(value);

	    probabilities.put(value, conditionalProbability);
	}

	normalize();

	// send pi messages to children

	JTextAreaLogger.instance().log("Sending pi messages to all children");
	for (Node n : getChildren()) {
	    SendPiMessageState newState = new SendPiMessageState();
	    newState.setSender(this);
	    newState.setReceiver(n);
	    newState.setAlgorithm(algorithm);
	    algorithm.pushAlgorithmState(newState);
	}
    }

    /**
     * This method is invoked on node that receives lambda message
     * 
     * @param sender
     *            is Node that sends message. Since lambda messages are sent up
     *            the three, this is child of node on which is this method
     *            invoked
     */
    public void sendLambdaMessageToThisNode(Node sender, Algorithm algorithm) {

	JTextAreaLogger.instance().log("Sending lambda message from " + sender.getName() + " to " + getName());
	// AlgorithmBlocker.instance().stopHere();

	for (String value : this.getValues()) {
	    // send lambda message
	    Double sum = new Double(0);
	    for (String valueOfSender : sender.getValues()) {
		sum += sender.getMyCPT().getProbability(this.getName(), valueOfSender, value) * sender.getLambdaValues().get(valueOfSender);
	    }

	    JTextAreaLogger.instance().log("Calculating lambda message: from " + sender.getName() + " to " + getName() + " - " + value + " = " + Util.getDoubleAsString(sum));
	    this.getLambdaMsgs().addLambdaMessage(sender.getName(), value, sum);
	    // AlgorithmBlocker.instance().stopHere();

	    // compute my new lambda values
	    Double product = new Double(1);
	    for (Node child : this.getChildren()) {
		product *= this.getLambdaMsgs().getLambdaMessage(child.getName(), value);
	    }

	    JTextAreaLogger.instance().log("Calculating lambda value for " + value + " in variable " + getName() + " = " + Util.getDoubleAsString(product));
	    this.getLambdaValues().put(value, product);
	    // AlgorithmBlocker.instance().stopHere();

	    // compute conditional probability
	    Double conditionalProbability = getLambdaValues().get(value) * getPiValues().get(value);

	    // JTextAreaLogger.instance().log("Calculating probability for variable "
	    // + getName() + " for " + value + ". Setting it to " +
	    // conditionalProbability);

	    probabilities.put(value, conditionalProbability);
	    // AlgorithmBlocker.instance().stopHere();
	}

	normalize();

	JTextAreaLogger.instance().log("Sending lambda message to parent");
	// AlgorithmBlocker.instance().stopHere();
	// here comes normalization
	if (getParents().size() > 0) {
	    Node parent = getParents().get(0); // it's tree
	    if (NodeAddressBook.instance().getRoot() != this && !RequestManager.instance().isNodeInRequest(parent)) {
		SendLambdaMessageState newState = new SendLambdaMessageState();
		newState.setAlgorithm(algorithm);
		newState.setSender(this);
		newState.setReceiver(parent);
		algorithm.pushAlgorithmState(newState);
		// parent.sendLambdaMessageToThisNode(this);
	    }
	}

	JTextAreaLogger.instance().log("Sending pi messages to children");
	// AlgorithmBlocker.instance().stopHere();
	for (Node child : getChildren()) {
	    if (child != sender && !RequestManager.instance().isNodeInRequest(child)) {
		// child.sendPiMessageToThisNode(this);
		SendPiMessageState newState = new SendPiMessageState();
		newState.setSender(this);
		newState.setReceiver(child);
		newState.setAlgorithm(algorithm);
		algorithm.pushAlgorithmState(newState);
	    }
	}

    }

    public String getStringCPT() {
	StringBuilder builder = new StringBuilder();

	if (getParents().size() > 0) {
	    Node parent = getParents().get(0);

	    for (String myValue : getValues()) {
		for (String parentValue : parent.getValues()) {
		    Double probability = myCPT.getProbability(parent.getName(), myValue, parentValue);

		    builder.append("Probability of ");
		    builder.append(myValue);
		    builder.append(" if given ");
		    builder.append(parentValue);
		    builder.append(" is ");
		    builder.append(Util.getDoubleAsString(probability));
		    builder.append("\n");
		}
	    }
	} else {
	    for (String myValue : getValues()) {
		Double probability = myCPT.getProbability(NO_PARENT, myValue, NO_CONDITION);

		builder.append("Probability of ");
		builder.append(myValue);
		builder.append(" is ");
		builder.append(Util.getDoubleAsString(probability));
		builder.append("\n");
	    }
	}

	return builder.toString();
    }

    public String getStringLambdaPiValues() {
	StringBuilder builder = new StringBuilder();

	for (String value : getValues()) {
	    Double lambda = lambdaValues.get(value);
	    Double pi = piValues.get(value);

	    builder.append(value);
	    builder.append(" ");
	    builder.append("->");
	    builder.append(" ");
	    builder.append("Lambda value: ");
	    builder.append(Util.getDoubleAsString(lambda));
	    builder.append(" Pi value: ");
	    builder.append(Util.getDoubleAsString(pi));
	    builder.append("\n");
	}

	return builder.toString();
    }

    public String getStringProbabilities() {
	StringBuilder builder = new StringBuilder();

	for (String valueName : probabilities.keySet()) {
	    Double probabilityForThisKey = probabilities.get(valueName);
	    builder.append("Probability for ");
	    builder.append(valueName);
	    builder.append(" is ");
	    builder.append(Util.getDoubleAsString(probabilityForThisKey));
	    builder.append("\n");
	}

	return builder.toString();

    }

    public String getStringLambdaPiMessages() {
	StringBuilder builder = new StringBuilder();

	// get lambda messages
	for (Node child : getChildren()) {
	    for (String myValue : getValues()) {
		Double lambdaMessage = lambdaMsgs.getLambdaMessage(child.getName(), myValue);
		builder.append("Lambda message from ");
		builder.append(child.getName());
		builder.append(" to ");
		builder.append(myValue);
		builder.append(" is ");
		builder.append(Util.getDoubleAsString(lambdaMessage));
		builder.append("\n");
	    }
	}

	// get pi messages
	for (Node parent : getParents()) {
	    for (String myValue : parent.getValues()) {
		Double piMessage = piMsgs.getPiMessage(parent.getName(), myValue);

		builder.append("Pi message from ");
		builder.append(parent.getName());
		builder.append(" (");
		builder.append(myValue);
		builder.append(") ");
		builder.append(" is ");
		builder.append(Util.getDoubleAsString(piMessage));
		builder.append("\n");

	    }
	}

	return builder.toString();
    }

    public void normalize() {
	Double sum = new Double(0);
	for (String key : probabilities.keySet()) {
	    Double oneValue = probabilities.get(key);
	    sum += oneValue;
	}

	for (String key : probabilities.keySet()) {
	    Double newValue = probabilities.get(key) / sum;
	    probabilities.put(key, newValue);
	    JTextAreaLogger.instance().log("Calculating probability for variable " + getName() + " for " + key + " = " + Util.getDoubleAsString(newValue));
	    // AlgorithmBlocker.instance().stopHere();
	}
    }

    public String getName() {
	return name;
    }

    public void setName(String name) {
	this.name = name;
    }

    public ConditionalProbabilityTable getMyCPT() {
	return myCPT;
    }

    public void setMyCPT(ConditionalProbabilityTable myCPT) {
	this.myCPT = myCPT;
    }

    public ArrayList<Node> getParents() {
	return parents;
    }

    public void setParents(ArrayList<Node> parents) {
	this.parents = parents;

	for (Node i : parents) {
	    HashMap<String, HashMap<String, Double>> map = new HashMap<String, HashMap<String, Double>>();
	    myCPT.getProbabilitiesMap().put(i.getName(), map);
	    for (String s : this.getValues()) {
		HashMap<String, Double> newMap = new HashMap<String, Double>();
		map.put(s, newMap);
	    }
	}
    }

    public ArrayList<Node> getChildren() {
	return children;
    }

    public void setChildren(ArrayList<Node> children) {
	this.children = children;
    }

    public ArrayList<String> getValues() {
	return values;
    }

    public void setValues(ArrayList<String> values) {
	this.values = values;
    }

    public LambdaMessages getLambdaMsgs() {
	return lambdaMsgs;
    }

    public void setLambdaMsgs(LambdaMessages lambdaMsgs) {
	this.lambdaMsgs = lambdaMsgs;
    }

    public HashMap<String, Double> getLambdaValues() {
	return lambdaValues;
    }

    public void setLambdaValues(HashMap<String, Double> lambdaValues) {
	this.lambdaValues = lambdaValues;
    }

    public void setProbability(String variableName, Double variableValue) {
	probabilities.put(variableName, variableValue);
    }

    public Double getProbability(String variableName) {
	return probabilities.get(variableName);
    }

    public HashMap<String, Double> getPiValues() {
	return piValues;
    }

    public void setPiValues(HashMap<String, Double> piValues) {
	this.piValues = piValues;
    }

    public PiMessages getPiMsgs() {
	return piMsgs;
    }

    public void setPiMsgs(PiMessages piMsgs) {
	this.piMsgs = piMsgs;
    }

    @Override
    public String toString() {
	return "{" + name + "}";
    }

    public ArrayList<String> getAllInsertsFromCPT() {
	ArrayList<String> result = new ArrayList<String>();

	if (this != NodeAddressBook.instance().getRoot()) { // if this is not a
							    // root
	    for (Node parent : getParents()) {
		for (String myValue : getValues()) {
		    for (String parentValue : parent.getValues()) {
			Double probability = myCPT.getProbability(parent.getName(), myValue, parentValue);
			String entry = parent.getName() + ", " + myValue + ", " + parentValue + ", " + Util.getDoubleAsString(probability);
			result.add(entry);
		    }
		}
	    }
	} else { // this is root
	    for (String myValue : getValues()) {
		Double probability = myCPT.getProbability(Node.NO_PARENT, myValue, Node.NO_CONDITION);
		String entry = Node.NO_PARENT + ", " + myValue + ", " + Node.NO_CONDITION + ", " + probability;
		result.add(entry);
	    }

	}

	return result;
    }
}
